dojo.provide("demo.widget.TreeComboBox");

dojo.require("dijit.form.ComboBox");
dojo.require("dijit.Tree");

dojo.declare("demo.widget.TreeComboBox", dijit.form.ComboBox, {
    // summary:
    //      Auto-completing text box with tree selector. Based on dojo 1.4.
    //
    // description:
    //      // TODO

    mode: "search", // "search" or "navigation"

    isSearchMode: function() {
        var result = (this.mode === "navigation") ? false : true;
        return result;
    },

    _popupTreeMenu: null, // popup widget for tree

    _pendingRefreshTree: false,
    _pendingRebuildTree: false,

    _topQuery: {}, // query for top items

    invalidQuery: {}, // query for non-selectable items

    // css classes for tree node icons
    iconClasses: {
        "opened": null,
        "closed": null,
        "leaf": null,
        "nonSelectableOpened": null,
        "nonSelectableClosed": null
    },

    nonSelectableLabelClass: null, // class name for non-selectable tree label

    /* @Override */
    postMixInProperties: function() {
        this._topQuery = this.query;
        this.inherited(arguments);
    },

    _setQueryAttr: function(query) {
        // summary:
        //      called by attr("query", query).
        //console.debug("_setQueryAttr", query);
        this.query = query;
        this._backupQuery = null;
        this._pendingRefreshTree = true;
    },

    _setStoreAttr: function(store) {
        // summary:
        //      called by attr("store", store).
        this.store = store;
        this._pendingRebuildTree = true;

        this.reset();
    },

    _getIdentityAttr: function() {
        // summary:
        //      called by attr("identity").
        var identity = null;
        var item = this.item;
        if (item) {
            identity = this.store.getIdentity(item);
        }

        return identity;
    },

    _setIdentityAttr: function(identity) {
        // summary:
        //      called by attr("identity", identity).
        if (!identity) {
            return;
        }

        var _self = this;
        this.store.fetchItemByIdentity({
            identity: identity,
            onItem: function(item) {
                if (item) {
                    var newValue = _self.labelFunc(item, _self.store);
                    _self.attr("item", item, false, newValue);
                    _self._setBlurValue(); // fire onChange
                }
            },
            onError: function() {
              console.warn("cannot find item");
            }
        });
    },

    _onKeyPress: function(/*Event*/ evt) {
        // summary:
        //      Handles keyboard events
        var key = evt.charOrCode;
        // except for cutting/pasting case - ctrl + x/v
        if (evt.altKey || ((evt.ctrlKey || evt.metaKey) && (key != 'x' && key != 'v')) || key == dojo.keys.SHIFT) {
            return; // throw out weird key combinations and spurious events
        }
        var doSearch = false;
        var searchFunction = "_startSearchFromInput";
        var dk = dojo.keys;
        this._prev_key_backspace = false;
        this._abortQuery();

        var highlighted = null;
        var popupMenu = null;
        if (this._isShowingNow) {
            popupMenu = (this.isSearchMode() === true) ? this._popupWidget : this._popupTreeMenu;
            popupMenu.handleKey(key);
            highlighted = popupMenu.getHighlightedOption();
        }
        switch (key) {
            case dk.PAGE_DOWN:
            case dk.DOWN_ARROW:
            case dk.PAGE_UP:
            case dk.UP_ARROW:
                if (!this._isShowingNow) {
                    // popup menu hidden & press up/down -> show tree menu
                    this.mode = "navigation";
                    this._arrowPressed();
                    doSearch = true;
                    searchFunction = "_startNavigation";
                } else {
                    // popup menu showing && press up/down -> move up/down in popup menu
                    this._announceOption(highlighted);
                }
                dojo.stopEvent(evt);
                break;
            case dk.LEFT_ARROW:
                // popup tree menu showing && press left -> collapse tree node
                if (this.isSearchMode() === false) {
                    // collapse focused tree node
                    if (this._isShowingNow) {
                        var tree = this._popupTreeMenu.tree;
                        var treeNode = tree.lastFocused;
                        tree._onLeftArrow({
                            node: treeNode,
                            item: treeNode.item
                        });
                        dojo.stopEvent(evt);
                    }
                }
                break;
            case dk.RIGHT_ARROW:
                // popup tree menu showing && press right -> expand tree node
                if (this.isSearchMode() === false) {
                    // expand focused tree node
                    if (this._isShowingNow) {
                        var tree = this._popupTreeMenu.tree;
                        var treeNode = tree.lastFocused;
                        tree._onRightArrow({
                            node: treeNode,
                            item: treeNode.item
                        });
                        dojo.stopEvent(evt);
                    }
                }
                break;
            case dk.ENTER:
                // prevent submitting form if user presses enter.
                if (highlighted) {
                    if (this.isSearchMode() === true) {
                        // prevent accepting the value if either Next or Previous are selected
                        // only stop event on prev/next
                        if (popupMenu && highlighted == popupMenu.nextButton) {
                            this._nextSearch(1);
                            dojo.stopEvent(evt);
                            break;
                        } else if (popupMenu && highlighted == popupMenu.previousButton) {
                            this._nextSearch(-1);
                            dojo.stopEvent(evt);
                            break;
                        }
                    }
                } else {
                    // Update 'value' (ex: KY) according to currently displayed text
                    this._setBlurValue(); // set value if needed
                    this._setCaretPos(this.focusNode, this.focusNode.value.length); // move cursor to end and cancel highlighting
                }
                // default case:
                // prevent submit, but allow event to bubble
                evt.preventDefault();
                // fall through

            case dk.TAB:
                if (this.isSearchMode() === true) {
                    var newValue = this.attr("displayedValue");
                    // if the user had More Choices selected fall into the _onBlur handler
                    if (popupMenu && (newValue === popupMenu._messages["previousMessage"] || newValue === popupMenu._messages["nextMessage"])) {
                        break;
                    }
                }

                if (highlighted) {
                    this._selectOption(highlighted);
                }
                if (this._isShowingNow) {
                    this._lastQuery = null; // in case results come back later
                    this._hideResultList();
                }
                break;

            case " ":
                if (highlighted) {
                    dojo.stopEvent(evt);
                    this._selectOption(highlighted);
                    this._hideResultList();
                } else {
                    doSearch = true;
                }
                break;

            case dk.ESCAPE:
                if (this._isShowingNow) {
                    dojo.stopEvent(evt);
                    this._hideResultList();
                }
                break;

            case dk.DELETE:
            case dk.BACKSPACE:
                this._prev_key_backspace = true;
                doSearch = true;
                break;

            default:
                // Non char keys (F1-F12 etc..)  shouldn't open list.
                // Ascii characters and IME input (Chinese, Japanese etc.) should.
                // On IE and safari, IME input produces keycode == 229, and we simulate
                // it on firefox by attaching to compositionend event (see compositionend method)
                doSearch = typeof key == "string" || key == 229;
        }
        if (doSearch) {
            // need to wait a tad before start search so that the event
            // bubbles through DOM and we have value visible
            this.item = undefined; // undefined means item needs to be set

            // restore query
            if (this._backupQuery) {
                this.query = dojo.clone(this._backupQuery);
            }
            this.searchTimer = setTimeout(dojo.hitch(this, searchFunction),1);
        }
    },

    /* @Override */
    _autoCompleteText: function(/*String*/ text) {
        // summary:
        //      Fill in the textbox with the first item from the drop down
        //      list, and highlight the characters that were
        //      auto-completed. For example, if user typed "CA" and the
        //      drop down list appeared, the textbox would be changed to
        //      "California" and "ifornia" would be highlighted.

        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            var fn = this.focusNode;

            // IE7: clear selection so next highlight works all the time
            //dijit.selectInputText(fn, fn.value.length);
            // does text autoComplete the value in the textbox?
            var caseFilter = this.ignoreCase ? 'toLowerCase' : 'substr';
            if (text[caseFilter](0).indexOf(this.focusNode.value[caseFilter](0)) == 0) {
                var cpos = this._getCaretPos(fn);
                // only try to extend if we added the last character at the end of the input
                if ((cpos + 1) > fn.value.length) {
                    // only add to input node as we would overwrite Capitalisation of chars
                    // actually, that is ok
                    fn.value = text;//.substr(cpos);
                    // visually highlight the autocompleted characters
                    dijit.selectInputText(fn, cpos);
                }
            } else {
                // text does not autoComplete; replace the whole value and highlight
                fn.value = text;
                dijit.selectInputText(fn);
            }
        }
    },

    /* @Override */
    _openResultList: function(/*Object*/ results, /*Object*/ dataObject) {
        // restore query
        if (this._backupQuery) {
            this.query = dojo.clone(this._backupQuery);
        }

        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            this._fetchHandle = null;
            if (this.disabled || this.readOnly) {
                return;
            }
            this._popupTreeMenu.clearResultList();

            this._updateTreeList();

            // show our list (only if we have content, else nothing)
            this._showResultList();

            // expand and focus selected tree node when combobox's popup menu is open
            var selectedItem = this.attr("item");
            var tree = this._popupTreeMenu.tree;
            if (selectedItem) {
                var isSyncWithTree = this._isItemSelectedInTree(selectedItem);
                //console.debug("isSyncWithTree", isSyncWithTree);
                if (isSyncWithTree === false) {
                    tree.setSelectedItem(selectedItem);
                } else {
                    tree.focusNode(tree.lastFocused);
                }
            } else {
                // clear tree selection
                tree._onHomeKey();
            }
        }
    },

    /* @Override */
    _showResultList: function() {
        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            this._hideResultList();
            this._arrowPressed();
            // hide the tooltip
            this.displayMessage("");

            // open popup menu and resize it
            this.resizePopupMenu(true);

            dijit.setWaiState(this.comboNode, "expanded", "true");
        }
    },

    /* @Override */
    _hideResultList: function() {
        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            // clear hover state
            this._clearTreeHoverState();

            // inherit
            this._abortQuery();
            if (this._isShowingNow) {
                dijit.popup.close(this._popupTreeMenu);
                this._arrowIdle();
                this._isShowingNow = false;
                dijit.setWaiState(this.comboNode, "expanded", "false");
                dijit.removeWaiState(this.focusNode, "activedescendant");
            }
        }
    },

    /* @Override */
    _setBlurValue: function() {
        // if the user clicks away from the textbox OR tabs away, set the
        // value to the textbox value
        // #4617:
        //      if value is now more choices or previous choices, revert
        //      the value
        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            var newValue = this.attr("displayedValue");

            if (typeof this.item == "undefined") {
                // Update 'value' (ex: KY) according to currently displayed text
                this.item = null;
                this.attr("displayedValue", newValue);
            } else {
                if (this.value != this._lastValueReported) {
                    dijit.form._FormValueWidget.prototype._setValueAttr.call(this, this.value, true);
                }
                this._refreshState();
            }
        }
    },

    /* @Override */
    _announceOption: function(/*TreeNode*/ treeNode) {
        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            // check if tree node is selectable
            var isSelectable = this._isSelectableOption(treeNode);
            if (!isSelectable) {
                return;
            }

            var item = treeNode.item;
            var newValue = this.labelFunc(item, this.store);
            this.attr("item", item, false, newValue);

            // get the text that the user manually entered (cut off autocompleted text)
            this.focusNode.value = this.focusNode.value.substring(0, this._lastInput.length);

            // autocomplete the rest of the option to announce change
            this._autoCompleteText(newValue);
        }
    },

    /* @Override */
    _selectOption: function(/*TreeNode*/ treeNode) {
        // summary:
        //      Menu callback function, called when an item in the menu is selected.

        if (this.isSearchMode() === true) {
            this.inherited(arguments);
        } else {
            if (treeNode) {
                // focus on selected tree node
                var tree = this._popupTreeMenu.tree;
                tree.focusNode(treeNode);

                // set item to combobox
                this._announceOption(treeNode);
            }

            this._hideResultList();
            this._setCaretPos(this.focusNode, this.focusNode.value.length);
            dijit.form._FormValueWidget.prototype._setValueAttr.call(this, this.value, true); // set this.value and fire onChange

            console.info("[OK]", "_selectOption->", this.value);
        }
    },

    /* @Override */
    _onArrowMouseDown: function(evt) {
        // summary:
        //      Callback when arrow is clicked
        if (this.disabled || this.readOnly) {
            return;
        }
        dojo.stopEvent(evt);
        this.focus();

        // close search result list menu
        if (this.isSearchMode() === true) {
            this._hideResultList();
        }

        this.mode = "navigation";
        if (this._isShowingNow) {
            this._hideResultList();
        } else {
            //this._backupQuery = dojo.clone(this.query); // backup query

            // forces full population of results, if they click
            // on the arrow it means they want to see more options
            this._startNavigation("");
        }
    },

    /* @Override */
    _startSearch: function(/*String*/ key) {
        // close tree menu
        if (this.isSearchMode() === false) {
            this._hideResultList();
        }

        //console.debug("_startSearch");
        this.mode = "search";
        this._backupQuery = dojo.clone(this.query); // backup current query

        // if search within all items, we need to change query
        var isDeepSearch = this._equals(this.query, this._topQuery);
        //console.debug("deep search", isDeepSearch);
        if (isDeepSearch) {
            this.query = {search: true}; // query all items of {search=true}
        }

        this.inherited(arguments);
    },

    _startNavigation: function(/*String*/ key) {
        //console.debug("_startNavigation");
        this.mode = "navigation";
        this._backupQuery = dojo.clone(this.query); // backup query

        key = key || "";

        if (!this._popupTreeMenu) {
            var popupId = this.id + "_popupTreeMenu";
            this._popupTreeMenu = new demo.widget._TreeComboBoxMenu({
                onChange: dojo.hitch(this, this._selectOption),
                onResize: dojo.hitch(this, this.resizePopupMenu),
                parentWidget: this,
                id: popupId
            });
            dijit.removeWaiState(this.focusNode,"activedescendant");
            dijit.setWaiState(this.textbox,"owns",popupId); // associate popup with textbox
        }

        //===== following remains same as ComboBox =====
        // create a new query to prevent accidentally querying for a hidden
        // value from FilteringSelect's keyField
        var query = dojo.clone(this.query); // #5970
        this._lastInput = key; // Store exactly what was entered by the user.
        this._lastQuery = query[this.searchAttr] = this._getQueryString(key);

        this._openResultList();
    },

    /* @Override */
    doHighlight: function(/*String*/label, /*String*/find) {
        // summary:
        //      Highlights the string entered by the user in the menu. By default this
        //      highlights the first occurence found. Override this method
        //      to implement your custom highlighing.
        // tags:
        //      protected

        // Add greedy when this.highlightMatch == "all"
        var modifiers = "i" + (this.highlightMatch == "all" ? "g": "");

        var escapedLabel = this._escapeHtml(label);
        find = dojo.regexp.escapeString(find); // escape regexp special chars

        var ret = escapedLabel.replace(new RegExp("("+ find +")", modifiers),
                '<span class="dijitComboBoxHighlightMatch">$1</span>');

        return ret;// returns String, (almost) valid HTML (entities encoded)
    },

    _isSelectableOption: function(/*TreeNode*/ treeNode) {
        // summary:
        //      Check if an option in the menu is selectable.

        if (!treeNode || !treeNode.item) {
            return false;
        }

        var isSelectableNode = true;
        var store = this.store;
        var item = treeNode.item;

        // check invalidQuery for non-selectable node
        var _invalidQuery = this.invalidQuery;
        for (var key in _invalidQuery) {
            var invalidValue = _invalidQuery[key];
            var value = store.getValue(item, key);

            if (value !== invalidValue) {
                // selectable
                isSelectableNode = true;
                break;
            } else {
                // non-selectable
                isSelectableNode = false;
            }
        }

        return isSelectableNode;
    },

    _isItemSelectedInTree: function(item) {
        var identity = this.store.getIdentity(item);

        var tree = this._popupTreeMenu.tree;
        var selectedItem = tree.attr("selectedItem");
        if (selectedItem) {
            var selectedIdentity = tree.model.getIdentity(selectedItem);
            if (identity === selectedIdentity) {
                // already selected
                return true;
            }
        }

        return false;
    },

    _updateTreeList: function() {
        // show complete tree if has selected item
        //if (this.item) {
        //    this.attr("query", this._topQuery);
        //}

        // create tree
        if (this._popupTreeMenu.isLoaded() === false) {
            // load tree for popup widget
            this._popupTreeMenu.load({
                widgetId: this.id,
                store: this.store,
                query: this.query
            });
            this._pendingRebuildTree = false; // clear flag
            this._pendingRefreshTree = false; // clear flag
            return;
        }

        // rebuild tree with new store
        if (this._pendingRebuildTree === true) {
            this._popupTreeMenu.refresh({
                store: this.store,
                query: this.query
            });
            this._pendingRebuildTree = false; // clear flag
            this._pendingRefreshTree = false; // clear flag
            return;
        }

        // refresh tree
        if (this._pendingRefreshTree === true) {
            this._popupTreeMenu.refresh({
                query: this.query
            });
            this._pendingRefreshTree = false; // clear flag
            return;
        }
    },

    _clearTreeHoverState: function() {
        var tree = this._popupTreeMenu.tree;
        if (!tree) {
            return;
        }

        for (var identity in tree._itemNodesMap) {
            var treeNodes = tree._itemNodesMap[identity];
            dojo.forEach(treeNodes, function(treeNode) {
                dojo.removeClass(treeNode.rowNode, "dijitTreeNodeHover");
            });
        }
    },

    _open: function() {
        // summary:
        //      Opens the drop down menu.
        // tags:
        //      private
        this._isShowingNow = true;
        var result = dijit.popup.open({
            popup: this._popupTreeMenu,
            around: this.domNode,
            parent: this
        });

        return result;
    },

    resizePopupMenu: function(isReOpen) {

        var menuNode = this._popupTreeMenu.domNode;

        // Position the list and if it's too big to fit on the screen then size it to the maximum possible height

        // natural size of the list has changed, so erase old width/height settings, which were hardcoded in a previous
        // call to this function (via dojo.marginBox() call)
        dojo.style(menuNode, {width: "", height: "", overflow: "auto"});

        var nodeBox = (isReOpen === true) ? this._open() : dojo.position(menuNode, true);

        // #4134:
        //      borrow TextArea scrollbar test so content isn't covered by
        //      scrollbar and horizontal scrollbar doesn't appear
        var width = nodeBox.w;
        if (nodeBox.h < menuNode.scrollHeight) {
            width += 20;
        }

        dojo.marginBox(menuNode, {
            h: nodeBox.h,
            w: Math.max(width, this.domNode.offsetWidth)
        });
        dojo.style(menuNode, {height: ""});
    },

    _equals: function(obj1, obj2) {
        // summary:
        //      function to determine if two objects has same properties.
        // tags:
        //      private
        var str1 = dojo.toJson(obj1);
        var str2 = dojo.toJson(obj2);

        return (str1 === str2);
    }
});

dojo.declare("demo.widget._TreeComboBoxMenu", dijit.form._ComboBoxMenu, {

    parentWidget: null,

    tree: null,

    /* @Override */
    templateString: dojo.cache("demo.widget", "templates/_TreeComboBoxMenu.html"),

    /* @Override */
    postCreate: function() {
        // empty
    },

    /* @Override */
    clearResultList: function() {
        // summary:
        //      Clears the entries in the drop down list.
    },

    /* @Override */
    _focusOptionNode: function(/*TreeNode*/ treeNode) {
        // summary:
        //      Does the actual highlight.
        var tree = this.tree;
        if (!treeNode) {
            treeNode = tree.lastFocused;
        }

        tree.focusNode(treeNode);
        dijit.scrollIntoView(treeNode.labelNode);
    },

    /* @Override */
    _highlightPrevOption: function() {
        // summary:
        //      Highlight the item just above the current selection.
        //      If nothing selected, highlight last option.
        //console.debug("_highlightPrevOption");
        var tree = this.tree;
        var treeNode = tree.lastFocused;

        if (!treeNode) {
            // select last node
            tree._onEndKey();
        } else {
            // select previous node
            tree._onUpArrow({
                node: treeNode,
                item: treeNode.item
            });
        }
    },

    /* @Override */
    _highlightNextOption: function() {
        // summary:
        //      Highlight the item just below the current selection.
        //      If nothing selected, highlight first option.
        //console.debug("_highlightNextOption");
        var tree = this.tree;
        var treeNode = tree.lastFocused;

        if (!treeNode) {
            // select first node
            tree._onHomeKey();
        } else {
            // select next node
            tree._onDownArrow({
                node: treeNode,
                item: treeNode.item
            });
        }
    },

    /* @Override */
    getHighlightedOption: function() {
        // summary:
        //      Returns the highlighted option item.
        var tree = this.tree;
        var treeNode = tree.lastFocused;
        return treeNode;
    },

    isLoaded: function() {
        return (!this.tree) === false;
    },

    load: function(args) {
        if (!this.tree) {
            var widgetId = args.widgetId;
            var store = args.store;
            var query = args.query;
            this._createTree(widgetId, store, query);
        }
    },

    refresh: function(args) {
        var store = args.store;
        var query = args.query || null;

        if (store) {
            console.info("new store");

            // update tree model
            this.tree.model.destroy();
            var modelArgs = this._getTreeModelArgs(store, query);
            this.tree.model.constructor(modelArgs);
        } else {
            console.info("new query");

            // update root node
            this.tree.model.query = query;
            this.tree.model._requeryTop();
        }

        // refresh tree
        this.tree.refresh();
    },

    _createTree: function(widgetId, store, query) {
        console.info("create tree", widgetId);

        var popupMenu = this;
        var parentWidget = popupMenu.parentWidget;

        var treeId = widgetId + "_tree";

        // find dom node to place the tree widget
        var nodes = dojo.query(".ComboBoxTree", this.domNode);
        if (nodes.length !== 1) {
            console.debug("cannot found tree dom node!");
            return;
        }
        var node = nodes[0];
        dojo.attr(node, "id", treeId);

        // init tree widget
        var modelArgs = this._getTreeModelArgs(store, query);
        this.tree = new demo.widget._ComboBoxTree({
            model: new dijit.tree.ForestStoreModel(modelArgs),
            showRoot: false,
            openOnClick: false, // clicking a folder node's label will call onClick()
            persist: false, // disable use of cookies for state saving
            iconClasses: parentWidget.iconClasses,
            nonSelectableLabelClass: parentWidget.nonSelectableLabelClass,
            isSelectableNode: function(/*TreeNode*/ node) {
                return parentWidget._isSelectableOption(node);
            },
            onClick: function(/* dojo.data */ item, /*TreeNode*/ node, /*Event*/ evt) {
                // check if non-selectable node
                var isSelectableNode = this.isSelectableNode(node);

                // return if non-selectable
                if (!isSelectableNode) {
                    return;
                }

                // call onChange
                popupMenu.onChange(node);
            },
            onOpen: dojo.hitch(this, this.onOpenTreeNode),
            onClose: dojo.hitch(this, this.onCloseTreeNode)
        },
        node);

        var _focusTreeNodeCallback = function() {
            var treeNode = this.tree.lastFocused;
            // empty
        };

        var _selectTreeNodeCallback = function() {
            var treeNode = this.tree.lastFocused;
            this.parentWidget._announceOption(treeNode);
            dijit.scrollIntoView(treeNode.labelNode);
        };

        dojo.connect(this.tree, "focusNode", this, _focusTreeNodeCallback);
        dojo.connect(this.tree, "_onUpArrow", this, _selectTreeNodeCallback);
        dojo.connect(this.tree, "_onDownArrow", this, _selectTreeNodeCallback);
        dojo.connect(this.tree, "_onLeftArrow", this, _selectTreeNodeCallback);
        dojo.connect(this.tree, "_onRightArrow", this, _selectTreeNodeCallback);
    },

    _getTreeModelArgs: function(store, query) {
        // create arguments for tree model
        var modelArgs = {
            store: store,
            query: query || null,
            rootId: "$root$",
            rootLabel: "ROOT",
            childrenAttrs: ["children"],
            deferItemLoadingUntilExpand: true
        };

        return modelArgs;
    },

    onOpenTreeNode: function(item, treeNode) {
        // treeNode.expand() returns a deferred object. We can hack in with our callback method.
        treeNode.expand().addCallback(dojo.hitch(this, function() {
            //console.debug("expando finished");
            this.onResize();
        }));
    },

    onCloseTreeNode: function(item, treeNode) {
        // treeNode.collapse() uses a wipeOut animation. We don't know when the animation will complete.
        // So we have to wait for a while to resize popup menu.
        setTimeout(dojo.hitch(this, function() {
            //console.debug("collapse finished");
            this.onResize();
        }), 300);
    },

    onResize: function() {
        // callback
    }
});

dojo.declare("demo.widget._ComboBoxTree", dijit.Tree, {

    iconClasses: {
        "opened": null,
        "closed": null,
        "leaf": null,
        "nonSelectableOpened": null, // class name for opened non-selectable node icon
        "nonSelectableClosed": null // class name for closed non-selectable node icon
    },

    nonSelectableLabelClass: null, // class name for non-selectable tree label

    /**
     * refresh the tree
     *
     * (many thanks to the tips from Rob Weiss found here:
     * http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-April/045180.html)
     */
    refresh: function() {
        // remove the rootNode
        if (this.rootNode) {
            this.rootNode.destroyRecursive();
        }

        // reset the state of the rootNode
        this.rootNode.state = "UNCHECKED";

        // rebuild the tree
        this._itemNodesMap = {};

        // load root node
        var refreshDeferred = new dojo.Deferred();

        var _onItem = dojo.hitch(this, function(item) {
            var rn = (this.rootNode = this.tree._createTreeNode({
                item: item,
                tree: this,
                isExpandable: true,
                label: this.label || this.getLabel(item),
                indent: this.showRoot ? 0 : -1
            }));

            if (!this.showRoot) {
                rn.rowNode.style.display = "none";
            }

            this.domNode.appendChild(rn.domNode);

            var identity = this.model.getIdentity(item);
            if (this._itemNodesMap[identity]) {
                this._itemNodesMap[identity].push(rn);
            } else {
                this._itemNodesMap[identity] = [rn];
            }

            rn._updateLayout(); // sets "dijitTreeIsRoot" CSS classname

            // update top level nodes
            this.model.store.fetch({
                query: this.model.query,
                onComplete: dojo.hitch(this, function(newChildren) {
                    this.model.root.children = newChildren;

                    // expand root node
                    this._expandNode(rn).addCallback(dojo.hitch(this, function() {
                        // this.onLoad();
                        refreshDeferred.callback(true);
                    }));
                })
            });
        });

        var _onError = function(err) {
            console.error(this, ": error loading root: ", err);
            refreshDeferred.errback(err);
        };

        this.model.getRoot(_onItem, _onError);

        return refreshDeferred;
    },

    isSelectableNode: function(/*TreeNode*/ node) {
        // summary:
        //      Overridable function to determine whether a tree node is selectable.
        // tags:
        //      extension
    },

    /* @Override */
    getIconClass: function(item, opened) {
        if (item === this.model.root) {
            return this.inherited(arguments);
        }

        var iconClass = null;

        var isSelectable = this.isSelectableNode({item: item});
        if (isSelectable === true) {
            iconClass = (!item || this.model.mayHaveChildren(item)) ? (opened ? this.iconClasses["opened"] : this.iconClasses["closed"]) : this.iconClasses["leaf"];
        } else {
            iconClass = opened ? this.iconClasses["nonSelectableOpened"] : this.iconClasses["nonSelectableClosed"];
        }

        if (!iconClass) {
            iconClass = this.inherited(arguments);
        }

        return iconClass;
    },

    /* @Override */
    getLabelClass: function(item, opened) {
        if (item === this.model.root) {
            return this.inherited(arguments);
        }

        var isSelectable = this.isSelectableNode({item: item});
        if (isSelectable === false && this.nonSelectableLabelClass) {
            return this.nonSelectableLabelClass;
        }

        return this.inherited(arguments);
    },

    setSelectedItem: function(item) {
        var identity = (!item || dojo.isString(item)) ? item : this.model.getIdentity(item);

        // find path from tree
        var trace = [];
        var history = {};
        var path = this._findPath(identity, this.model, trace, this.model.root, history);
        console.debug("*** FINISHED: path", path, "trace", trace);
        console.dir(path);

        if (path && path.length > 0) {
            // item found, focus its node
            this.attr("path", path);
        } else {
            // item not found, focus first node
            this._onHomeKey();
        }
    },

    /**
     * Find a path for the target node in the tree, from top to bottom.
     * Usage: var path = this._findPath(identity, this.model, trace, this.model.root);
     * Note: The performance might be poor because it will travise the tree until it finds the target node.
     *
     * @param {String} identity
     *         The identity of the node.
     * @param {TreeStoreModel} model
     *         The model of the tree.
     * @param {Array} trace
     *         The path used to recursive.
     * @param {Item} item
     *         The item of the parent node.
     * @param {Object} history
     *         A Map to record the find history.
     *         It will help to avoid endless recursion when a parent and a child reference each other.
     * @return The path for the target node.
     */
    _findPath: function(identity, model, trace, item, history) {
        //console.log(">> recursiveHunt, item ", item, " looking for ", identity);
        var id = model.getIdentity(item);
        trace.push(id);

        if (history[id] === true) {
            //console.debug("history:", id);
            return null;
        }

        history[id] = true;
        if (id === identity) {
            // Return the trace array, indicating a match was found
            console.info("++ FOUND item ", item, " trace now = ", trace);
            return trace;
        }
        for (var idx in item.children) {
            // start a new branch of trace, starting with what we have so far
            var traceBranch = trace.slice(0);
            //console.log("Branching into ", model.store.getValue(item.children[idx], "name"), ", traceBranch=", traceBranch);
            var pathBranch = this._findPath(identity, model, traceBranch, item.children[idx], history);
            // If a match was found in that recurse, return it.
            //  This unwinds the recursion on completion.
            if (pathBranch) {
                return pathBranch;
            }
        }
        // Return null, indicating no match was found
        return null;
    },

    /**
     * Find a path for the target node in the tree, from bottom to top.
     * Usage: var path = this._findPath(item, trace);
     *
     * @param {Item} item
     *         The item of the node.
     * @param {Array} path
     *         The path used to recursive.
     * @return The path for the target node.
     * @deprecated Cannot work if top node is not shown in the tree.
     */
    $_findPath: function(item, path) {

        var identity = this.model.getIdentity(item);
        path.unshift(identity); // insert first

        if (!item.parents || item.parents.length === 0) {
            var isTop = (item.top === true);
            if (!isTop) {
                // find top node
                var rootItem = this.model.root;
                var topItem = null;
                for (var idx in rootItem.children) {
                    topItem = item.children[idx];

                    // current item should be one of the children of top item
                    if (dojo.isArray(topItem.children)) {
                        var found = dojo.some(topItem.children, function(candidateItem) {
                            if (this.model.getIdentity(candidateItem)) {
                                return true;
                            }
                            return false;
                        }, this);

                        if (found) {
                            var topIdentity = this.model.getIdentity(topItem);
                            path.unshift(topIdentity); // insert first
                            break;
                        }
                    }
                }
            }

            return path;
        }

        var parentItem = item.parents[0];
        return this._findPath(parentItem, path);
    }
});